package sa.Master.Server.Modules;

import sa.Master.common.Classes.*;
import sa.Master.common.Functions.*;
import sa.Master.Server.Server.Field;

import com.google.gson.*;
import com.google.gson.stream.*;

import java.io.*;

public class PropertyManager {
    
    // serverRoot/data/ == dataRoot
    private File dataRoot;

    public PropertyManager(File serverRoot, long timeout) {

        this.dataRoot = new File(serverRoot, "data");
        LockManager.timeout = timeout;

    }

    private File getTablesDirectory(Field field, String subjectName) throws FileNotFoundException {

        StringBuilder targetStringBuilder = new StringBuilder();
        StringBuilder errorStringBuilder = new StringBuilder("tables: ");

        // String building
        if (field == Field.personal) {
            targetStringBuilder.append("personal");
            errorStringBuilder.append("User ");
        } else {
            targetStringBuilder.append("group");
            errorStringBuilder.append("Group ");
        }
        targetStringBuilder.append(String.format("/%s", subjectName));
        errorStringBuilder.append(String.format("%s's tables directory: Not found.", subjectName));

        File target = new File(dataRoot, targetStringBuilder.toString());

        // Returns
        if (!target.isDirectory()) {
            throw new FileNotFoundException(String.format(errorStringBuilder.toString()));
        } else {
            return target;
        }

    }

    private File getItemsDirectory(File tablesDirectory, String tableName) throws FileNotFoundException {

        File target = new File(tablesDirectory, tableName);
        if (!target.isDirectory()) {
            throw new FileNotFoundException(String.format("show items: table \"%s\" of %s's: Not found.", 
            tableName, 
            tablesDirectory.getName()));
        } else {
            return target;
        }

    }

    private File getItemFile(File itemsDirectory, String itemName) throws FileNotFoundException, IOException {

        File target = new File(itemsDirectory, itemName);
        StringBuilder errorMessagBuilder = new StringBuilder("show item: ");
        if (!target.isFile()) {
            errorMessagBuilder.append(String.format("item \"%s\" in table \"%s\" of %s's: Not found.", 
            itemName, 
            itemsDirectory.getName(), 
            itemsDirectory.getParentFile().getName()));
            throw new FileNotFoundException(errorMessagBuilder.toString());
        } else if (!target.canRead()) {
            errorMessagBuilder.append(String.format("item \"%s\" in table \"%s\" of %s's: Server file error.", 
            itemName, 
            itemsDirectory.getName(), 
            itemsDirectory.getParentFile().getName()));
            throw new IOException(errorMessagBuilder.toString());
        } else {
            return target;
        }

    }

    public String showTables(Field field, String subjectName) throws FileNotFoundException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        Gson g = new Gson();
        return g.toJson(tablesDir.list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return new File(dir, name).isDirectory();
            }
        }));

    }

    public String showItems(Field field, String subjectName, String tableName) throws FileNotFoundException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File tableDir = this.getItemsDirectory(tablesDir, tableName);

        Gson g = new Gson();
        return g.toJson(tableDir.list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return !(new File(dir, name).isDirectory());
            }
        }));

    }

    public String showItem(Field field, String subjectName, String tableName, String itemName) throws FileNotFoundException, IOException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File tableDir = this.getItemsDirectory(tablesDir, tableName);
        File itemFile = this.getItemFile(tableDir, itemName);

        try {
            StringBuilder builder = new StringBuilder();
            String line = null;
            BufferedReader in = new BufferedReader(new FileReader(itemFile));
            
            while ((line=in.readLine()) != null) {
                builder.append(line);
                builder.append("\n");
            }

            in.close();
            return builder.toString();
        } catch (IOException e) {
            throw new IOException(String.format("show item: item \"%s\" in table \"%s\" of %s's: Server file error.", 
            itemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        }

    }

    public void createTable(Field field, String subjectName, String newTableName) throws IOException, DuplicateException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File newTableDir = new File(tablesDir, newTableName);
        if (newTableDir.isDirectory()) {
            throw new DuplicateException(String.format("create table: table \"%s\" of %s's: Already exists.",
            newTableName, 
            subjectName));
        } else if (!newTableDir.mkdir()) {
            throw new IOException(String.format("create table: table \"%s\" of %s's: Server error.", 
            newTableName, 
            subjectName));
        }

        // image Dir
        if (!(new File(newTableDir, "image").mkdir())) {
            LoggingUtilities.syserr("warning: create table: image directory creation failed.");
        }

    }

    public void createItem(Field field, String subjectName, String tableName, String newItemName, String newItemJson) throws IOException, JsonParseException, DuplicateException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File tableDir = this.getItemsDirectory(tablesDir, tableName);
        File newItemFile = new File(tableDir, newItemName);

        if (newItemFile.exists()) {
            throw new DuplicateException(String.format("create item: item \"%s\" in table \"%s\" of %s's: Already exists.",
            newItemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        }

        // Write new Json File
        try {
            String prettifiedJson = JsonFormat.prettify(newItemJson);
            BufferedWriter out = new BufferedWriter(new FileWriter(newItemFile));
            out.write(prettifiedJson, 0, prettifiedJson.length());
            out.close();
        } catch (JsonParseException | MalformedJsonException e) {
            throw new JsonParseException(String.format("create item: item \"%s\" in table \"%s\" of %s's: Json bad format.", 
            newItemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        } catch (IOException e) {
            throw new IOException(String.format("create item: item \"%s\" in table \"%s\" of %s's: Server file error.", 
            newItemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        }

    }

    public void createGroup(String groupName) throws IOException, DuplicateException {

        try {
            File tablesDir = this.getTablesDirectory(Field.group, groupName);
        } catch (FileNotFoundException e) {
            // can successfully create group
            File target = new File(dataRoot, "group/"+groupName);
            if (!target.mkdir()) {
                throw new IOException("create group: Server error.");
            }
        }
        throw new DuplicateException(String.format("create group: Group %s exists.", groupName));

    }

    public void deleteTable(Field field, String subjectName, String deleteTableName) throws IOException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File deleteTableDir = this.getItemsDirectory(tablesDir, deleteTableName);
        
        if (!removeDirectory.remove(deleteTableDir)) {
            throw new IOException(String.format("delete table: table \"%s\" of %s's: Server error.", 
            deleteTableName, 
            subjectName));
        }

    }

    public void deleteItem(Field field, String subjectName, String tableName, String deleteItemName) throws IOException, LockException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File tableDir = this.getItemsDirectory(tablesDir, tableName);
        File deleteItemFile = this.getItemFile(tableDir, deleteItemName);

        if (!(LockManager.getLock(tableDir, deleteItemName))) {
            LockManager.removeLock(tableDir, deleteItemName);

            throw new LockException(String.format("delete item: item \"%s\" in table \"%s\" of %s's: Lock error.", 
            deleteItemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        }

        if (!(deleteItemFile.delete()) && deleteItemFile.exists()) {
            LockManager.removeLock(tableDir, deleteItemName);
            throw new IOException(String.format("delete item: item \"%s\" in table \"%s\" of %s's: Server file error.", 
            deleteItemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        }
        LockManager.removeLock(tableDir, deleteItemName);

    }

    public void updateItem(Field field, String subjectName, String tableName, String itemName, String updateItemJson) throws IOException, JsonParseException, LockException {

        File tablesDir = this.getTablesDirectory(field, subjectName);
        File tableDir = this.getItemsDirectory(tablesDir, tableName);
        File updateItemFile = this.getItemFile(tableDir, itemName);

        if (!(LockManager.getLock(tableDir, itemName))) {
            LockManager.removeLock(tableDir, itemName);

            throw new LockException(String.format("update item: item \"%s\" in table \"%s\" of %s's: Lock error.", 
            itemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        }

        // Update new Json File
        try {
            if (!updateItemFile.delete()) {
                throw new IOException();
            }
            String prettifiedJson = JsonFormat.prettify(updateItemJson);
            BufferedWriter out = new BufferedWriter(new FileWriter(updateItemFile));
            out.write(prettifiedJson, 0, prettifiedJson.length());
            out.close();
        } catch (JsonParseException | MalformedJsonException e) {
            throw new JsonParseException(String.format("update item: item \"%s\" in table \"%s\" of %s's: Json bad format.", 
            itemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        } catch (IOException e) {
            throw new IOException(String.format("update item: item \"%s\" in table \"%s\" of %s's: Server file error.", 
            itemName, 
            tableDir.getName(), 
            tablesDir.getName()));
        } finally {
            LockManager.removeLock(tableDir, itemName);
        }

    }

}

class LockManager {

    public static long timeout;

    synchronized private static File getLockFile(File itemsDirectory, String itemName) {

        return new File(itemsDirectory, itemName+".lock");

    }

    synchronized public static boolean lockExists(File lockFile) {

        boolean flag = true;

        try {
            DataInputStream input = new DataInputStream(new FileInputStream(lockFile));
            long lockCreatedTime = input.readLong();
            input.close();
            long now = System.currentTimeMillis();
            if (now-lockCreatedTime >= timeout) {
                lockFile.delete();
                flag = false;
            }
        } catch (FileNotFoundException e) {
            flag = false;
        } catch (IOException e) {
            flag = true;
        }
        return flag;

    }

    synchronized public static boolean getLock(File itemsDirectory, String itemName) {

        boolean flag = false;
        File lockFile = getLockFile(itemsDirectory, itemName);

        if (!lockExists(lockFile)) {
            try {
                DataOutputStream output = new DataOutputStream(new FileOutputStream(lockFile));
                output.writeLong(System.currentTimeMillis());
                output.close();
                flag = true;
            } catch (IOException e) {
                lockFile.delete();
                flag = false;
            }
        }
        return flag;

    }

    synchronized public static void removeLock(File itemsDirectory, String itemName) {
        File lockFile = getLockFile(itemsDirectory, itemName);

        if (lockExists(lockFile)) {
            lockFile.delete();
        }
    }

}
